-
Notifications
You must be signed in to change notification settings - Fork 917
GODRIVER-3605 Refactor StringN #2128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
API Change Report./v2/x/bsonx/bsoncoreincompatible changesArray.StringN: changed from func(int) string to func(int) (string, bool) |
f7afe10
to
050c014
Compare
f9a5d13
to
4d17e8b
Compare
d2d4214
to
0563a2b
Compare
|
||
// If the last byte is not a closing bracket, then the document was truncated | ||
if len(str) > 0 && str[len(str)-1] != '}' { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line causes GODRIVER-3561
acac4b7
to
620794a
Compare
620794a
to
3d68a75
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent work, Qingyang. Thank you for taking this on 👍
x/bsonx/bsoncore/document.go
Outdated
|
||
var buf strings.Builder | ||
// stringN stringify a document. If N is larger than 0, it will truncate the string to N bytes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we clarify in the method documentation that the second parameter indicates if the stringified document was truncated?
func (d Document) StringN(n int) string { | ||
if len(d) < 5 || n <= 0 { | ||
return "" | ||
func (d Document) StringN(n int) (string, bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we clarify in the method documentation that the second parameter indicates if the stringified document was truncated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use named return values.
E.g.
func (d Document) StringN(n int) (val string, truncated bool)
Edit: Note that named return values are only generally a good idea on short functions (<50 lines). Disregard this suggestion if StringN
becomes longer than 50 lines.
x/bsonx/bsoncore/document.go
Outdated
if n <= 0 { | ||
if l, _, ok := ReadLength(d); !ok || l < 5 { | ||
return "", false | ||
} | ||
return "", true | ||
} | ||
return d.stringN(n) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's interesting that calling StringN(0)
will return an empty string but stringN(0)
will return the entire document. The asymmetry makes the code fairly difficult to read. Should we update StringN
so that an N <= 0 argument returns the full document?
func (d Document) String(n int) (string, bool) {
if n <= 0 {
return d.stringN(0)
}
return d.stringN(n)
}
Ultimately, it's unclear to me why one would call StringN(0)
expecting an empty string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By using stringN()
, we can keep the original behavior of StringN(n)
, which is to return an empty string when n <= 0, while also allowing it to be reused by String()
. However, I will be happy to merge stringN()
and StringN()
if we all accept returning the entire document on StringN(0)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that makes sense. Will let @matthewdale opine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a slight preference for -1 (i.e. all negative) vs 0 (i.e. all non-positive) because it more closely matches the semantics of string truncation (i.e. StringN(0) == String()[:0]
), although there's not a clear use case for passing either -1 or 0.
Is there a reason we want to change the behavior from the original where we pass math.IntMax
to get the full document?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-1 (i.e. all negative) vs 0 (i.e. all non-positive) seems a more sensible interface.
It's just for a better compatibility that not using a particular value like math.IntMax
to get the entire document.
x/bsonx/bsoncore/document.go
Outdated
if !ok || length < 5 { | ||
return "", false | ||
} | ||
length -= (4 /* length bytes */ + 1 /* final null byte */) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] Can we re-write this to use //
?
length := (4 + // length bytes
1) // final null bytes
truncatedStr := bsoncoreutil.Truncate(str, n-buf.Len()) | ||
buf.WriteString(truncatedStr) | ||
|
||
for length > 0 && !truncated { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] The loop logic is hard to follow, suggest adding comments. I think the following are sufficient:
- determine remaining budget
l := n - buf.Len()
- If
buf.Len() >= n
, then mark truncated and break - if
!first
, then write comma, then decrementl
and possibly break - ReadElement, subtract its full length, exit on
!ok
- Delegate to
elem.stringN(l)
, write its str, record its truncated flag
l = n - buf.Len() | ||
} | ||
if !first { | ||
buf.WriteByte(',') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure we should be writing the comma here? Without testing directly it seems unintuitive. What if n=1
or something and there isn't enough room for a comma?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another issue is that we don't need a comma if the document only contains one element, even if there is enough space.
x/bsonx/bsoncore/document_test.go
Outdated
for _, tc := range testCases { | ||
for n := -1; n <= len(tc.want)+1; n++ { | ||
t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { | ||
got, _ := tc.doc.StringN(n) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should validate the expected values for the the truncation bool.
x/bsonx/bsoncore/element.go
Outdated
func (e Element) StringN(n int) string { | ||
func (e Element) StringN(n int) (string, bool) { | ||
if n <= 0 { | ||
return "", len(e) > 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider treating n<=0
as "no limit", see comment on document.StringN().
x/bsonx/bsoncore/value.go
Outdated
if n <= 0 { | ||
return "" | ||
return "", len(str) > 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider treating n<=0 as "no limit", see comment on document.StringN().
x/bsonx/bsoncore/array.go
Outdated
if lens, _, _ := ReadLength(a); lens < 5 || n <= 0 { | ||
return "" | ||
func (a Array) StringN(n int) (string, bool) { | ||
if n <= 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider treating n<=0 as "no limit", see comment on document.StringN().
x/bsonx/bsoncore/array_test.go
Outdated
|
||
for _, tc := range testCases { | ||
for n := -1; n <= len(tc.want)+1; n++ { | ||
t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider factoring these into two test functions TestArray_String
and TestArray_StringN
and moving the test cases to a package-level variable (e.g. var arrayStringTestCases = ...
).
x/bsonx/bsoncore/document_test.go
Outdated
|
||
for _, tc := range testCases { | ||
for n := -1; n <= len(tc.want)+1; n++ { | ||
t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider factoring these into two test functions TestDocument_String
and TestDocument_StringN
and moving the test cases to a package-level variable (e.g. var documentStringTestCases = ...
).
x/bsonx/bsoncore/value_test.go
Outdated
t.Run(tc.description, func(t *testing.T) { | ||
got := tc.val.StringN(tc.n) | ||
for n := -1; n <= len(tc.want)+1; n++ { | ||
t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider factoring these into two test functions TestValue_String
and TestValue_StringN
and moving the test cases to a package-level variable (e.g. var valueStringTestCases = ...
).
x/bsonx/bsoncore/value_test.go
Outdated
{15, `"𨉟呐㗂越"`}, | ||
{21, `"𨉟呐㗂越"`}, | ||
} { | ||
t.Run(fmt.Sprintf("StringN multi-byte string n==%d", tc.n), func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider factoring this into a separate test function TestArray_StringN_Multibyte
.
buf.WriteByte(']') | ||
} | ||
|
||
return buf.String() | ||
return buf.String(), truncated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to truncate the string in the buffer?
E.g.
bsoncoreutil.Truncate(buf.String())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not technically necessary, as the buffer has been truncated previously. However, an extra-truncating sounds like a good idea for an additional layer of security
buf.WriteByte('}') | ||
} | ||
|
||
return buf.String() | ||
return buf.String(), truncated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to truncate the string in the buffer?
E.g.
bsoncoreutil.Truncate(buf.String())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not technically necessary, as the buffer has been truncated previously. However, an extra-truncating sounds like a good idea for an additional layer of security
74d2883
to
555951f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
This reverts commit a709a1d.
GODRIVER-3605
GODRIVER-3561
Summary
Refactor
StringN
Background & Motivation
Originally,
Element.StringN()
does not truncate strings as expected.This PR also adds a return flag to
StringN()
to indicate whether the string has been truncated.